/*
*    l3qd - Light, light, lightweight queue
*    Copyright (C) 2024  Marcus Pedersén marcus@marcux.org
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

// Contains functionality for
// the validate daemon
package validate

import (
	"bufio"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/sethvargo/go-password/password"
	"golang.org/x/term"
	"gorm.io/gorm"
	"net"
	"net/http"
	"notabug.org/marcux/l3q/internal/common"
	"notabug.org/marcux/l3q/internal/daemon"
	"notabug.org/marcux/l3q/internal/daemon/sql"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"
)

// Struct holding info for
// a host and functions for
// the API
type Host struct {
	password  string
	ip        string
	hostname  string
	name      string
	key       string
	daemonKey string
	db        *gorm.DB
}

// Called if URL is invalid
func wrongUrl(c *gin.Context) {
	c.JSON(http.StatusNotFound, gin.H{})
}

func commonValidate(c *gin.Context, host *Host) error {
	pw := c.Query("password")

	if pw != host.password {
		return fmt.Errorf("Password does not match")
	}

	host.ip = c.ClientIP()

	hostNames, err := net.LookupAddr(host.ip)
	if err != nil {
		return fmt.Errorf("Failed to look up hostname: %s", err)
	}

	if len(hostNames) > 0 {
		host.hostname = hostNames[0]
	}

	hKey, hErr := password.Generate(50, 20, 0, false, true)
	if hErr != nil {
		return fmt.Errorf("Failed to generate host key: %s", hErr)
	}

	dKey, kErr := password.Generate(50, 20, 0, false, true)
	if kErr != nil {
		return fmt.Errorf("Failed to generate daemon key: %s", kErr)
	}

	host.key = hKey
	host.daemonKey = dKey

	return nil
}

// Validate a compute node
// Query parameters:
// password: specified at start up of daemon
// port: port that node daemon is
// listening on.
//
// JSON response:
// If Status OK
// key=Server generated key
// On error empty JSON returned.
func (h Host) validateNode(c *gin.Context) {
	err := commonValidate(c, &h)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Validation error:\n%s\n", err)
		c.JSON(http.StatusInternalServerError, gin.H{})
		return
	}

	strPort := c.Query("port")
	if len(strPort) == 0 {
		fmt.Fprintf(os.Stderr, "Query parameter: port is missing\n")
		c.JSON(http.StatusBadRequest, gin.H{})
		return
	}

	port, pErr := strconv.Atoi(strPort)
	if pErr != nil {
		fmt.Fprintf(os.Stderr, "Query parameter: port is not a valid integer: %s\n", strPort)
		c.JSON(http.StatusBadRequest, gin.H{})
		return
	}

	node := common.Node{}

	node.Ip = h.ip
	node.Port = port
	node.Hostname = h.hostname
	node.Name = h.name
	node.Key = h.key
	node.DaemonKey = h.daemonKey

	err = sql.InsertNode(h.db, node)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s\n", err)
		c.JSON(http.StatusInternalServerError, gin.H{})
	}

	resp := common.KeyResponse{Key: h.key}
	fmt.Println("Validation SUCCESS!!")
	c.JSON(http.StatusOK, resp)
}

// Validate a client node
// Query parameters:
// password: specified at start up of daemon
//
// JSON response:
// If Status OK
// key=Server generated key
// On error empty JSON returned.
func (h Host) validateClient(c *gin.Context) {
	err := commonValidate(c, &h)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Validation error:\n%s\n", err)
		c.JSON(http.StatusInternalServerError, gin.H{})
		return
	}

	client := sql.Client{}
	client.Ip = h.ip
	client.Name = h.name
	client.Hostname = h.hostname
	client.Key = h.key
	client.DaemonKey = h.daemonKey

	err = sql.InsertClient(h.db, client)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s\n", err)
		c.JSON(http.StatusInternalServerError, gin.H{})
		return
	}

	resp := common.KeyResponse{Key: h.key}
	fmt.Println("Validation SUCCESS!!")
	c.JSON(http.StatusOK, resp)
}

// Starts the validate daemon
func ValidateD(conf daemon.Config) {
	db, err := sql.InitDb(conf)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s", err)
		os.Exit(2)
	}

	host := Host{db: db}

	listen, err := net.Listen("tcp", fmt.Sprintf("%s:", conf.IpListen))
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to open port for validate daemon: %s\n", err)
		os.Exit(3)
	}

	splitAddr := strings.Split(listen.Addr().String(), ":")

	if len(splitAddr) == 1 {
		fmt.Fprintf(os.Stderr, "Failed to parse choosen port\n")
		os.Exit(3)
	}

	strPort := splitAddr[len(splitAddr)-1]
	listen.Close()

	port, err := strconv.Atoi(strPort)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to convert port: %s to integer.\n", strPort)
		os.Exit(3)
	}

	fmt.Printf("Validate server will start on port: %d\n", port)
	fmt.Println("")
	fmt.Println("#### INSTRUCTIONS ####")
	fmt.Println("Specify temporary password that should be used on the host")
	fmt.Println("when promped for.")

	for {
		fmt.Print("Enter temporary password: ")
		bytepw, err := term.ReadPassword(int(syscall.Stdin))
		if err != nil {
			fmt.Fprintf(os.Stderr, "\nFailed to read password: %s, please try again.\n\n", err)
			continue
		}

		pw := string(bytepw)

		if len(pw) == 0 {
			fmt.Fprintf(os.Stderr, "\nEmpty password is not allowed, please try again.\n\n")
		} else {
			host.password = pw
			break
		}
	}

	fmt.Println("\nSpecify name (optional), to more easy recognise host.")
	fmt.Print("Name: ")
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	host.name = strings.TrimSpace(scanner.Text())

	fmt.Println("")
	fmt.Println("Go to client host or compute node and run as root:")
	fmt.Printf("Client: l3q validate --port %d\n", port)
	fmt.Println("Node: node-l3qd --validate")
	fmt.Println("Enter the same password at the prompt.")
	fmt.Println("")
	fmt.Println("When command prints: Validation SUCCESS!!")
	fmt.Println("Validation is done and you can enter CTRL+C to stop this validation server.")
	fmt.Println("")

	route := gin.Default()
	route.NoRoute(wrongUrl)
	route.NoMethod(wrongUrl)
	route.GET("/api/v1/validate/client", host.validateClient)
	route.GET("/api/v1/validate/node", host.validateNode)

	route.RunTLS(fmt.Sprintf("%s:%d", conf.IpListen, port),
		filepath.Join(conf.CertDir, "cert.pem"),
		filepath.Join(conf.CertDir, "key.pem"))

}
